/**
 * Copyright (c) 2001-2012 Steve Purcell.
 * Copyright (c) 2002      Vidar Holen.
 * Copyright (c) 2002      Michal Ceresna.
 * Copyright (c) 2005      Ewan Mellor.
 * Copyright (c) 2010-2012 penSec.IT UG (haftungsbeschränkt).
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer. Redistributions in binary
 * form must reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other materials provided
 * with the distribution. Neither the name of the copyright holder nor the names
 * of its contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package bma.common.langutil.jmshell.cmdopt;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * Largely GNU-compatible command-line options parser. Has short (-v) and
 * long-form (--verbose) option support, and also allows options with associated
 * values (-d 2, --debug 2, --debug=2). Option processing can be explicitly
 * terminated by the argument '--'.
 * 
 * @author Steve Purcell
 * @author penSec.IT UG (haftungsbeschränkt)
 * 
 * @version 2.0
 * @see com.sanityinc.jargs.examples.OptionTest
 */
public class CommandOptionParser {

	/**
	 * Add the specified Option to the list of accepted options
	 */
	public <T> Option<T> addOption(Option<T> opt) {
		if (opt.shortForm() != null) {
			this.options.put("-" + opt.shortForm(), opt);
		}
		this.options.put("--" + opt.longForm(), opt);
		return opt;
	}

	/**
	 * Convenience method for adding a string option.
	 * 
	 * @return the new Option
	 */
	public Option<String> addStringOption(char shortForm, String longForm,
			String tips) {
		return addOption(new StringOption(shortForm, longForm, tips));
	}

	/**
	 * Convenience method for adding an integer option.
	 * 
	 * @return the new Option
	 */
	public Option<Integer> addIntegerOption(char shortForm, String longForm,
			String tips) {
		return addOption(new IntegerOption(shortForm, longForm, tips));
	}

	/**
	 * Convenience method for adding a long integer option.
	 * 
	 * @return the new Option
	 */
	public Option<Long> addLongOption(char shortForm, String longForm,
			String tips) {
		return addOption(new LongOption(shortForm, longForm, tips));
	}

	/**
	 * Convenience method for adding a double option.
	 * 
	 * @return the new Option
	 */
	public Option<Double> addDoubleOption(char shortForm, String longForm,
			String tips) {
		return addOption(new DoubleOption(shortForm, longForm, tips));
	}

	/**
	 * Convenience method for adding a boolean option.
	 * 
	 * @return the new Option
	 */
	public Option<Boolean> addBooleanOption(char shortForm, String longForm,
			String tips) {
		return addOption(new BooleanOption(shortForm, longForm, tips));
	}

	public boolean hasOption(String name) {
		for (Option<?> op : options.values()) {
			if (op.is(name))
				return true;
		}
		return false;
	}

	public Option<?> getOption(String name) {
		for (Option<?> op : options.values()) {
			if (op.is(name))
				return op;
		}
		return null;
	}

	public <T> T getOptionValue(String name, T def) {
		@SuppressWarnings("unchecked")
		Option<T> o = (Option<T>) getOption(name);
		return getOptionValue(o, def);
	}

	/**
	 * Equivalent to {@link #getOptionValue(Option, Object) getOptionValue(o,
	 * null)}.
	 */
	public <T> T getOptionValue(Option<T> o) {
		return getOptionValue(o, null);
	}

	/**
	 * @return the parsed value of the given Option, or the given default 'def'
	 *         if the option was not set
	 */
	public <T> T getOptionValue(Option<T> o, T def) {
		if (o == null)
			return def;
		List<?> v = values.get(o.longForm());

		if (v == null) {
			return def;
		} else if (v.isEmpty()) {
			return null;
		} else {

			/*
			 * Cast should be safe because Option.parseValue has to return an
			 * instance of type T or null
			 */
			@SuppressWarnings("unchecked")
			T result = (T) v.remove(0);
			return result;
		}
	}

	public <T> Collection<T> getOptionValues(String name, Class<T> cls) {
		@SuppressWarnings("unchecked")
		Option<T> o = (Option<T>) getOption(name);
		return getOptionValues(o);
	}

	/**
	 * @return A Collection giving the parsed values of all the occurrences of
	 *         the given Option, or an empty Collection if the option was not
	 *         set.
	 */
	public <T> Collection<T> getOptionValues(Option<T> option) {
		Collection<T> result = new ArrayList<T>();

		while (true) {
			T o = getOptionValue(option, null);

			if (o == null) {
				return result;
			} else {
				result.add(o);
			}
		}
	}

	/**
	 * @return the non-option arguments
	 */
	public String[] getRemainingArgs() {
		return this.remainingArgs;
	}

	/**
	 * Extract the options and non-option arguments from the given list of
	 * command-line arguments. The default locale is used for parsing options
	 * whose values might be locale-specific.
	 */
	public void parse(String[] argv) throws OptionException {
		parse(argv, Locale.getDefault());
	}

	public String parse(String text) throws OptionException {
		return parse(text, Locale.getDefault());
	}

	public String parse(String text, Locale locale) throws OptionException {
		List<String> args = new ArrayList<String>();
		int c = text.length();
		int st = 0; // 0-normal, 1-space, 2-"'
		int pos = 0;
		char sp = 0;
		for (int i = 0; i < c; i++) {
			char ch = text.charAt(i);
			// String ts = new String(new char[] { ch });
			// System.out.println(ts);
			if (Character.isSpaceChar(ch)) {
				switch (st) {
				case 1:
					pos = i + 1;
					break;
				case 2:
					break;
				case 0: {
					st = 1;
					if (pos < i) {
						String s = text.substring(pos, i);
						args.add(s);
						pos = i + 1;
					}
					break;
				}
				}
				continue;
			}

			if (ch == '"' || ch == '\'') {
				switch (st) {
				case 0:
				case 1: {
					if (pos < i) {
						String s = text.substring(pos, i);
						args.add(s);
					}
					pos = i + 1;
					st = 2;
					sp = ch;
					break;
				}
				case 2: {
					if (sp == ch) {
						if (pos < i) {
							String s = text.substring(pos, i);
							args.add(s);
							pos = i + 1;
						}
						st = 0;
					}
					break;
				}
				}
				continue;
			}
			if (st == 1)
				st = 0;
		}

		if (pos < c) {
			String s = text.substring(pos);
			args.add(s);
		}
		String r = null;
		if (args.size() > 0) {
			r = args.remove(0);
		}
		parse(args.toArray(new String[0]), locale);
		return r;
	}

	/**
	 * Extract the options and non-option arguments from the given list of
	 * command-line arguments. The specified locale is used for parsing options
	 * whose values might be locale-specific.
	 */
	public void parse(String[] argv, Locale locale) throws OptionException {

		ArrayList<Object> otherArgs = new ArrayList<Object>();
		int position = 0;
		this.values = new HashMap<String, List<?>>(10);
		while (position < argv.length) {
			String curArg = argv[position];
			if (curArg.startsWith("-")) {
				if (curArg.equals("--")) { // end of options
					position += 1;
					break;
				}
				String valueArg = null;
				if (curArg.startsWith("--")) { // handle --arg=value
					int equalsPos = curArg.indexOf("=");
					if (equalsPos != -1) {
						valueArg = curArg.substring(equalsPos + 1);
						curArg = curArg.substring(0, equalsPos);
					}
				} else if (curArg.length() > 2) { // handle -abcd
					for (int i = 1; i < curArg.length(); i++) {
						Option<?> opt = this.options
								.get("-" + curArg.charAt(i));
						if (opt == null) {
							throw new UnknownSuboptionException(curArg,
									curArg.charAt(i));
						}
						if (opt.wantsValue()) {
							throw new NotFlagException(curArg, curArg.charAt(i));
						}
						addValue(opt, null, locale);

					}
					position++;
					continue;
				}

				Option<?> opt = this.options.get(curArg);
				if (opt == null) {
					throw new UnknownOptionException(curArg);
				}

				if (opt.wantsValue()) {
					if (valueArg == null) {
						position += 1;
						if (position < argv.length) {
							valueArg = argv[position];
						}
					}
					addValue(opt, valueArg, locale);
				} else {
					addValue(opt, null, locale);
				}

				position += 1;
			} else {
				otherArgs.add(curArg);
				position += 1;
			}
		}
		for (; position < argv.length; ++position) {
			otherArgs.add(argv[position]);
		}

		this.remainingArgs = new String[otherArgs.size()];
		remainingArgs = otherArgs.toArray(remainingArgs);
	}

	private <T> void addValue(Option<T> opt, String valueArg, Locale locale)
			throws IllegalOptionValueException {

		T value = opt.getValue(valueArg, locale);
		String lf = opt.longForm();

		/*
		 * Cast is typesafe because the only location we add elements to the
		 * values map is in this method.
		 */
		@SuppressWarnings("unchecked")
		List<T> v = (List<T>) values.get(lf);

		if (v == null) {
			v = new ArrayList<T>();
			values.put(lf, v);
		}

		v.add(value);
	}

	public String getHelpString(String cmd, String args) {
		StringBuilder sb = new StringBuilder();
		sb.append("usage: ").append(cmd);
		if (!options.isEmpty()) {
			sb.append(" [options]");
		}
		if (args != null) {
			sb.append(" ").append(args);
		}

		if (!options.isEmpty()) {
			sb.append("\n");
			Set<Option<?>> set = new TreeSet<Option<?>>(
					new Comparator<Option<?>>() {
						public int compare(Option<?> o1, Option<?> o2) {
							return o1.longForm().compareTo(o2.longForm());
						};
					});
			set.addAll(options.values());
			Iterator<Option<?>> it = set.iterator();
			while (it.hasNext()) {
				Option<?> op = it.next();
				sb.append(op.getHelpString());
				if (it.hasNext())
					sb.append("\n");
			}
		}
		return sb.toString();
	}

	private String[] remainingArgs = null;
	private Map<String, Option<?>> options = new HashMap<String, Option<?>>(10);
	private Map<String, List<?>> values = new HashMap<String, List<?>>(10);
}
